package pl.edu.mimuw.irs.core.model.analysis.resources;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.Map.Entry;

import pl.edu.mimuw.irs.core.global.IDManager;
import pl.edu.mimuw.irs.core.model.analysis.TPair;
import pl.edu.mimuw.irs.core.model.analysis.dd.ActivityWrapper;
import pl.edu.mimuw.irs.core.model.design.resource.Requirement;
import pl.edu.mimuw.irs.core.model.design.resource.Requirements;
import pl.edu.mimuw.irs.core.model.design.resource.Role;
import pl.edu.mimuw.irs.core.model.petrinet.Transition;

public class Hire extends Transition {

	private static final long serialVersionUID = 12337332283673236L;

	// *** Attributes

	private ActivityWrapper activity;
	private ResourcesGraph graph;
	private Map<ResourcePlace, TPair> hiringMap;
	public ActivityWrapper getActivity() {
		return activity;
	}
	public ResourcesGraph getGraph() {
		return graph;
	}
	public Map<ResourcePlace, TPair> getHiringMap() {
		if (hiringMap == null) hiringMap = 
			new LinkedHashMap<ResourcePlace, TPair>();
		return hiringMap;
	}
	
	// *** Constructors
	
	public Hire(IDManager idManager, ActivityWrapper activity, ResourcesGraph graph,
			Map<ResourcePlace, TPair> hiringMap) {
		super(idManager, idManager.getPrefixedNewId("Hire" + activity.getActivity().getName()), 
				activity.getActivity().getPosition().left());
		this.activity = activity;
		this.graph = graph;
		this.hiringMap = hiringMap;
	}
	
	// *** Methods
	
	@Override
	public boolean invoke() {
		/*
		 * Now we get some random of applicable resources and check if our
		 * choice covers the activity. If yes - we can hire them to proceed 
		 * with corresponding activity.
		 */
		Requirements requirements = getActivity().getActivity().getRequirement().getRequirements();
		Map<ResourcePlace, Set<RequirementPlace>> forHire = null;// FIXME : random is bugged! getResourcesForHire(requirements);
		
		if (forHire == null) {
			forHire = getBruteForceResourcesForHire(requirements);
		}
		
		if (forHire == null) {
			return false;
		} else {
			try {
				hire(forHire);
			} catch (Exception ex) {
				ex.toString();
			}
			getActivity().setStarted(true);
			
			return super.invoke();
		}
	}
	
	protected Map<ResourcePlace, Set<RequirementPlace>> getResourcesForHire( // TODO : should be private
			Requirements requirements) {
		/* 
		 * We use sets, because we assume that resource cannot have the same 
		 * role/requirement met twice.
		 */
		Map<RequirementPlace, Set<ResourcePlace>> requirementToResources =
			new LinkedHashMap<RequirementPlace, Set<ResourcePlace>>();
		List<RequirementPlace> randomized = 
			new ArrayList<RequirementPlace>(requirements.size());
		Map<ResourcePlace, Set<RequirementPlace>> forHire = 
			new LinkedHashMap<ResourcePlace, Set<RequirementPlace>>();

		/*
		 * Creating map of possible matchings of resources to requirements 
		 * and vice versa.
		 */
		for (Requirement requirement : requirements) {
			/*
			 * Check who in general is able to cope with this requirement.
			 */
			
			RequirementPlace requirementPlace = graph.getRequirements().get(requirement);
			randomized.add(requirementPlace);
			Set<ResourcePlace> meetingResources = requirementPlace.getMeetingResources();
			for (ResourcePlace resource : meetingResources) {
				/*
				 * Resource that can cope with requirement might be busy at this
				 * time, so we have to make sure it is not.
				 */
				if (isAvailable(resource) &&
						resource.meets(requirement)) {
					Set<ResourcePlace> toResources = requirementToResources.get(requirementPlace);
					if (toResources == null) {
						toResources = new LinkedHashSet<ResourcePlace>();
						requirementToResources.put(requirementPlace, toResources);
					}
					toResources.add(resource);
				}
			}
		}
		
		/*
		 * Maps prepared - we can start the lottery. First - prepare the
		 * requirements in randomized order.
		 */
		Collections.shuffle(randomized);
		Random random = new Random();
		
		/*
		 * For each of these requirements try to get some resource.
		 */
		for (RequirementPlace requirementPlace : randomized) {
			Set<ResourcePlace> resources = requirementToResources.get(requirementPlace);
			/* 
			 * Possibly there is no resource for this requirement - so our hiring 
			 * failed. However it does not mean there is no other possibility for the
			 * resources to be correctly matched to our requirements.
			 */
			if (resources == null || resources.size() == 0) return null;
			/*
			 * Choose one random resource and remove all applicable roles/requirements
			 * from maps and so, to make sure it will not be used twice where not
			 * possible.
			 */
			ResourcePlace[] array = new ResourcePlace[resources.size()];
			resources.toArray(array);
			ResourcePlace randomResource = array[random.nextInt(array.length)];
			Set<RequirementPlace> matched = forHire.get(randomResource);
			if (matched == null) {
				matched = new LinkedHashSet<RequirementPlace>();
				forHire.put(randomResource, matched);
			}
			matched.add(requirementPlace);
			Set<Role> availableRoles = randomResource.getAvailableRoles();
			//Set<RequirementPlace> affected = getAffected(availableRoles, requirementPlace);
			Set<RequirementPlace> affected = getAffected(availableRoles, randomized);
			for (RequirementPlace affectedRequirement : affected) {
				Set<ResourcePlace> toResources = requirementToResources.get(affectedRequirement);
				if (toResources == null) continue;
				toResources.remove(randomResource);
				if (toResources.size() == 0) requirementToResources.remove(affectedRequirement);
			}
		}
		
		return forHire;
	}
	
	private Map<ResourcePlace, Set<RequirementPlace>> getBruteForceResourcesForHire(
			Requirements requirements) {
		/* 
		 * We use sets, because we assume that resource cannot have the same 
		 * role/requirement met twice.
		 */
		Map<RequirementPlace, Set<ResourcePlace>> requirementToResources =
			new LinkedHashMap<RequirementPlace, Set<ResourcePlace>>();
		List<RequirementPlace> randomized = 
			new ArrayList<RequirementPlace>(requirements.size());
		Map<ResourcePlace, Set<RequirementPlace>> forHire = 
			new LinkedHashMap<ResourcePlace, Set<RequirementPlace>>();

		/*
		 * Creating map of possible matchings of resources to requirements 
		 * and vice versa.
		 */
		for (Requirement requirement : requirements) {
			/*
			 * Check who in general is able to cope with this requirement.
			 */
			
			RequirementPlace requirementPlace = graph.getRequirements().get(requirement);
			randomized.add(requirementPlace);
			Set<ResourcePlace> meetingResources = requirementPlace.getMeetingResources();
			for (ResourcePlace resource : meetingResources) {
				/*
				 * Resource that can cope with requirement might be busy at this
				 * time, so we have to make sure it is not. We also must know that
				 * all roles from this requirement have not been assigned to any
				 * other requirement during this hiring process.
				 */
				if (isAvailable(resource) &&
						resource.meets(requirement)) {
					Set<ResourcePlace> toResources = requirementToResources.get(requirementPlace);
					if (toResources == null) {
						toResources = new LinkedHashSet<ResourcePlace>();
						requirementToResources.put(requirementPlace, toResources);
					}
					toResources.add(resource);
				}
			}
		}
		
		/*
		 * Maps prepared - we can start the lottery. First - prepare the
		 * requirements in randomized order.
		 */
		Collections.shuffle(randomized);
		
		/*
		 * Recursively try to get some resource.
		 */
		if (!getAssignments(randomized, 0, requirementToResources, forHire)) return null;
		
		return forHire;
	}
	
	private boolean getAssignments(List<RequirementPlace> randomized, int i,
			Map<RequirementPlace, Set<ResourcePlace>> requirementToResources,
			Map<ResourcePlace, Set<RequirementPlace>> forHire) {
		if (i == randomized.size()) return true;
		RequirementPlace requirementPlace = randomized.get(i);
		
		Set<ResourcePlace> resources = requirementToResources.get(requirementPlace);
		/* 
		 * Possibly there is no resource for this requirement - so our hiring 
		 * failed. However it does not mean there is no other possibility for the
		 * resources to be correctly matched to our requirements.
		 */
		if (resources == null || resources.size() == 0) return false;
		/*
		 * Choose one random resource and remove all applicable roles/requirements
		 * from maps and so, to make sure it will not be used twice where not
		 * possible.
		 */
		ResourcePlace[] array = new ResourcePlace[resources.size()];
		List<ResourcePlace> randomizedResources = Arrays.asList(resources.toArray(array));
		Collections.shuffle(randomizedResources);
		
		for (ResourcePlace randomResource : randomizedResources) {
			Set<RequirementPlace> matched = forHire.get(randomResource);
			if (matched == null) {
				matched = new LinkedHashSet<RequirementPlace>();
				forHire.put(randomResource, matched);
			}
			matched.add(requirementPlace);
			Set<Role> requirementRoles = requirementPlace.getRequirement().getRolesMap().keySet();//randomResource.getAvailableRoles();
			//Set<RequirementPlace> metRequirements = randomResource.getMetRequirements();
			//Set<RequirementPlace> affected = getAffected(availableRoles, requirementPlace);
			Set<RequirementPlace> affected = getAffected(requirementRoles, randomized);
			Set<RequirementPlace> affectedForSure = new LinkedHashSet<RequirementPlace>();
			for (RequirementPlace affectedRequirement : affected) {
				Set<ResourcePlace> toResources = requirementToResources.get(affectedRequirement);
				if (toResources == null) continue;
				affectedForSure.add(affectedRequirement);
				toResources.remove(randomResource);
				if (toResources.size() == 0) requirementToResources.remove(affectedRequirement);
			}
			
			/* 
			 * If we are here it means we successfully matched all resources to requirements so far.
			 * Now we have invoke the search recursively and check if any is possible. If not - try
			 * other assignment.
			 */
			
			boolean possible = getAssignments(randomized, i + 1, requirementToResources, forHire);
			if (possible) {
				/*
				 * It means we are in correct hiring path, and we can now just return the 
				 * result (forHire contains now all hiring matchings) and exit this loop.
				 */
				return true;
			} else {
				/* 
				 * Here we must have entered the incorrect path - the resources cannot be assigned.
				 * We have to revert the changes made in this step and try next resource.
				 */
				matched.remove(requirementPlace);
				if (matched.size() == 0) {
					forHire.remove(randomResource);
				}
				/* 
				 * Now we should restore the mappings which were modified by above for loop.
				 */
				for (RequirementPlace affectedRequirement : affectedForSure) {
					Set<ResourcePlace> toResources = requirementToResources.get(affectedRequirement);
					if (toResources == null) {
						toResources = new LinkedHashSet<ResourcePlace>();
						requirementToResources.put(affectedRequirement, toResources);
					}
					toResources.add(randomResource);
				}
				
				continue;
			}
		}
		
		/*
		 * Here we must have failed to get any assignment.
		 */
		return false;
	}
	
	private Set<RequirementPlace> getAffected(Set<Role> roles,
			List<RequirementPlace> randomized) {
		Set<RequirementPlace> affected = new LinkedHashSet<RequirementPlace>();
		//for (Role role : roles) {
		//	if (randomized.getRequirement().getRolesMap().containsKey(role)) {
		//		affected.add(randomized);
		//		break;
		//	}
			
		for (RequirementPlace suspected : randomized) {
			Set<Role> suspectedRoles = suspected.getRequirement().getRolesMap().keySet();
			if (!Collections.disjoint(roles, suspectedRoles)) affected.add(suspected);
		}
		//}
		
		return affected;
	}
	
	private boolean isAvailable(ResourcePlace resource) {
		return (resource.getTokens() > 0) ||
			(getActivity().isDerived(resource));
	}
	
	private void hire(Map<ResourcePlace, Set<RequirementPlace>> forHire) {
		for (Entry<ResourcePlace, Set<RequirementPlace>> entry : forHire.entrySet()) {
			entry.getKey().hireTo(getActivity(), entry.getValue());
		}
	}
}
